import { Editor, EditorTheme } from "./editor.js";
import * as THREE from 'three';
import { Viewport } from "./viewport.js";
import { AddObjectCommand } from "./commands/AddObjectCommand.js";
import * as compoennts from "./components.js";
import { MaterialType, TezeroMode } from "./enums.js";
import { Keyboard } from "./keyboard.js";
import { EditorCamera } from "./camera.js";
import { Material, MaterialParameters, MeshBasicMaterial, MeshLambertMaterial, MeshStandardMaterial, Object3D, Sprite } from "three";
import { Player,View } from "./player.js";
import { TezeroMQTT } from "./data/mqtt.js";
import { SetMaterialCommand } from "./commands/SetMaterialCommand.js";
import {compress, decompress} from "../index";
import { ViewportInfo } from "./viewport.info.js";
import { ViewportShading } from "./viewport.shading.js";
import { SetValueCommand } from "./commands/SetValueCommand.js";
import {RectAreaLightUniformsLib} from "three/examples/jsm/lights/RectAreaLightUniformsLib";

import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { CoordinateConverter } from "./utils/coordinate.js";
import { CSS3DObject } from "three/examples/jsm/renderers/CSS3DRenderer.js";
import { DomObject, createBoxWithCSS3DChild } from "./utils/object.js";

const DATA_TRANSFER_KEY='tezero_object';

export interface TezeroOptions{
  /**
   * 模式 默认: edit
   */
  mode?:TezeroMode;

  /**
   * 编辑器主题
   */
  theme?:EditorTheme;

  /**
   * 选中物体辅助颜色
   */
  selectColor?:THREE.ColorRepresentation;

  /**
   * class
   */
  className?:string;

  pixelRatio?:number;
  /**
   * 控制器属性
   */
  control?:{
    enabled:boolean;
    panSpeed:number;
	  zoomSpeed:number;
    rotationSpeed:number;
  }
  
  /**
   * 是否显示辅助对象
   */
  showHelper?:boolean;
  /**
   * 是否显示网格
   */
  showGrid?:boolean;

  /**
     * shader precision. Can be "highp", "mediump" or "lowp".
     */
   precision?: string | undefined;

   /**
    * default is false.
    */
   alpha?: boolean | undefined;

   /**
    * default is true.
    */
   premultipliedAlpha?: boolean | undefined;

   /**
    * default is false.
    */
   antialias?: boolean | undefined;

   /**
    * default is true.
    */
   stencil?: boolean | undefined;

   /**
    * default is false.
    */
   preserveDrawingBuffer?: boolean | undefined;

   /**
    * Can be "high-performance", "low-power" or "default"
    */
   powerPreference?: string | undefined;

   /**
    * default is true.
    */
   depth?: boolean | undefined;

   /**
    * default is false.
    */
   logarithmicDepthBuffer?: boolean | undefined;

   /**
    * default is false.
    */
   failIfMajorPerformanceCaveat?: boolean | undefined;
}


const defaultTezeroOptions:TezeroOptions={
  mode:TezeroMode.Edit,
  theme:EditorTheme.System,
  className:'',
  pixelRatio:window.devicePixelRatio,
  control:{
    enabled:true,
    zoomSpeed:0.01,
    panSpeed:0.001,
    rotationSpeed:0.003
  },
  showHelper:true,
  showGrid:true,
  selectColor:0x409eff,
}


export class Tezero {
  el:HTMLElement;
  container:HTMLElement;
  options:TezeroOptions;
  // private components?:TezeroCommponents=defaultComponents;
  editor:Editor;
  private viewport:Viewport;
  private renderer:THREE.WebGLRenderer;
  private keyboard?:Keyboard;
  private cameraSelector?:HTMLElement;
  private mqtt:TezeroMQTT;
  /**
   * 添加物体之前钩子函数
   */
  public onAddObjectBefore?: (compoennt: TezeroCommponent, object: THREE.Object3D) => boolean;
  /**
   * 添加物体之后钩子函数
   */
  public onAddObjectAfter?: (compoennt: TezeroCommponent, object: THREE.Object3D) => void;

  /**
   * DOM元素3d对象
   */
  static DomObject=DomObject;

  /**
   * @see Tezero.DomObject
   * @deprecated
   */
  static createDomObject=createBoxWithCSS3DChild;
  
  constructor(el:HTMLElement,options?:TezeroOptions) {
    RectAreaLightUniformsLib.init();
    this.el=el;
    this.options=Object.assign(defaultTezeroOptions,options);
    this.container=document.createElement('div');
    this.container.className=`tezero ${this.options.mode} ${this.options.className}`;
    this.container.style.position='relative';
    this.container.style.width='100%';
    this.container.style.height='100%';
    // this.container.setAttribute('tabindex','0');
    this.el.appendChild(this.container);

    this.editor=new Editor(this.options);
    if(options.theme){
      this.editor.theme=this.options.theme;
    }

    this.viewport=new Viewport(this.editor,this.options);
    this.viewport.mode=this.options.mode||TezeroMode.Edit;
    if(this.options.showHelper!==undefined){
      this.editor.signals.showHelpersChanged.dispatch(this.options.showHelper);
    }
    if(this.options.showGrid!==undefined){
      this.editor.signals.showGridChanged.dispatch(this.options.showHelper);
    }
    const player=Player(this.editor);
    player.style.top='0px';
    player.style.left='0px';
    player.style.width='100%';
    player.style.height='100%';
    this.container.appendChild(this.viewport.dom);
    this.container.appendChild(this.viewport.stats.dom);

    const info=ViewportInfo(this.editor);
    this.container.appendChild(info);

    const shading=ViewportShading(this.editor);
    this.container.appendChild(shading);
    this.container.appendChild(player);

    this.viewport.dom.addEventListener('click',()=>{
      this.viewport.dom.focus();
    })

    // this.mqtt=new TezeroMQTT(this.editor);


    this.renderer = new THREE.WebGLRenderer({antialias: true,logarithmicDepthBuffer: true,precision:this.options.precision,alpha: true });
    this.editor.signals.rendererCreated.dispatch(this.renderer);

    if(this.options.mode===TezeroMode.Edit){
      // this.keyboard=new Keyboard(this.editor,this.container);
      this.keyboard=new Keyboard(this.editor);
      this.cameraSelector=EditorCamera(this.editor);
      this.container.append(this.cameraSelector);
      this.container.addEventListener('dragover',this.onDragover.bind(this));
      this.container.addEventListener('drop',this.onDrop.bind(this));
    }else if(this.options.mode===TezeroMode.Run){
      this.startPlayer();
    }
    
    window.addEventListener( 'resize', this.onWindowResize.bind(this) );
    this.onWindowResize();
    
  }

  private onWindowResize(){
    this.editor.signals.windowResize.dispatch();
  }
  private onDragover(e:DragEvent){
    e.stopPropagation();
    e.preventDefault();
  }
  
  private onDrop(e:DragEvent){
    const json=e.dataTransfer?.getData(Tezero.DragData);
    if(json){
      const data=JSON.parse(json);
      const converter = new CoordinateConverter(this.renderer);
      const worldPos = converter.screenToWorldOnPlane(
        e.clientX,
        e.clientY,
        this.editor.camera,
        new THREE.Plane(new THREE.Vector3(0, 1, 0), 0)
      );
      data.position=worldPos;
      this.add(data);
      this.viewport.dom.focus();
    }
    
  }

  /**
   * 加载HDR环境盒到场景
   * @param url 
   */
  loadHDR(url:string){
    const hdrLoader=new RGBELoader();
    
    hdrLoader.loadAsync(url).then((texture)=>{
      // 设置天空球面的纹理映射
      // 三等距柱状投影反射映射告诉渲染器环境映射采用等距柱状投影格式。由于着色器仅支持非 PBR 的立方体贴图格式和 PBR 材质的立方体 UV 格式，因此必须转换等距柱状投影纹理才能进行渲染。这是由渲染器自动完成的。
      texture.mapping=THREE.EquirectangularReflectionMapping;
      this.editor.signals.sceneBackgroundChanged.dispatch(texture);
      this.editor.signals.sceneEnvironmentChanged.dispatch(texture);
    });
  }

  /**
   * 设置是否显示辅助对象
   * @param showSceneHelpers 
   */
  setShowHelper(showHelper:boolean){
    this.editor.signals.showHelpersChanged.dispatch(showHelper);
  }
  /**
   * 设置是否显示网格
   * @param showSceneHelpers 
   */
  setShowGrid(showGrid:boolean){
    this.editor.signals.showGridChanged.dispatch(showGrid);
  }

  /**
   * 销毁编辑器
   */
  destroy(){
    delete this.editor.components;
    if(this.keyboard){
      this.keyboard.destroy();
    }
    if(this.cameraSelector){
      this.container.removeChild(this.cameraSelector);
    }
    this.editor.clear();
    this.renderer.dispose();
    this.container.removeChild(this.viewport.dom);
    this.container.removeEventListener('dragover',this.onDragover);
    this.container.removeEventListener('drop',this.onDrop);
    this.el.removeChild(this.container);
  }

  /**
   * 注册组件
   * @param compoennts 物体组件数据
   */
  register(compoennts:TezeroCommponents){
    if(this.editor.components)
    Object.assign(this.editor.components,compoennts);
  }

  /**
   * 物体组件数据生成三维物体并添加到场景中
   * @param compoennt 网格组件数据
   * @param parent 
   * @returns 三维物体
   */
  add(compoennt:TezeroCommponent,parent?:THREE.Object3D):THREE.Object3D|undefined{
    if(!this.editor.components){
      throw new Error('无注册的组件');
    }
    const handler=this.editor.components[compoennt.type];
    if(handler===undefined){
      throw new Error(`未知的组件类型: ${compoennt.type}`);
    }
    const object=handler(compoennt);
    if(object instanceof Object3D){
      return this._add(compoennt,object);
    }
    if(object instanceof Promise){
      object.then(obj=>{
        this._add(compoennt,obj);
      }).catch(e=>{
        throw e;
      });
    }
    
  }

  private _add(compoennt:TezeroCommponent,obj:THREE.Object3D):THREE.Object3D|undefined{
    setBaseAttr(obj,compoennt);
    if(this.onAddObjectBefore){
      const canAdd=this.onAddObjectBefore(compoennt,obj);
      if(canAdd===undefined||canAdd===null||canAdd===false){
        return undefined;
      }
    }
    this.editor.execute(new AddObjectCommand(this.editor, obj));
    if(this.onAddObjectAfter){
      this.onAddObjectAfter(compoennt,obj);
    }
    return obj;
  }

  get scene(){
    return this.editor.scene;
  }

  /**
   * uuid获取对象
   * @param uuid Object3D对象的uuid
   * @returns 
   */
  getObject(uuid:string){
    return this.editor.scene.getObjectByProperty('uuid',uuid);
  }

  /**
   * 选中物体，传null取消选中
   * @param object uuid或Object3D对象
   */
  select(object?:string|Object3D|null){
    if(object&&typeof object==='string'){
      const target=this.getObject(object);
      this.editor.signals.objectSelected.dispatch(target);
    }else{
      this.editor.signals.objectSelected.dispatch(object);
    }
    
  }


  setAnimation(uuid:string,animations:TezeroAnimation[]){
    const object=this.editor.scene.getObjectByProperty('uuid',uuid);
    if(object!==undefined){
      if(object.userData===undefined){
        object.userData={};
      }
      object.userData.animations=animations;
    }
  }

  /**
   * 运行场景
   */
  startPlayer(){
    if(this.cameraSelector)
    this.cameraSelector.style.display='none';
    this.editor.signals.startPlayer.dispatch();
  }
  /**
   * 停止运行场景
   */
  stopPlayer(){
    if(this.cameraSelector)
    this.cameraSelector.style.display='block';
    this.editor.signals.stopPlayer.dispatch();
  }

  connectMqtt(brokerUrl:string,topic:string|string[]){
    // this.mqtt.connect(brokerUrl);
  }

  /**
   * 修改2D文字物体信息
   * @param uuid 
   * @param data 
   * @returns 
   */
  setText(uuid:string,data:TezeroCommponent){
    const object=this.editor.scene.getObjectByProperty('uuid',uuid);
    if(!object)return;
    if(object.userData&&object.userData.isText!==true){
      console.warn('不能修改非二维文字');
      return;
    }
    var newData=data;
    if(object.userData.text!==undefined&&object.userData.text!=null){
      newData=Object.assign(object.userData.text,data);  
    }
    const text=compoennts.text2d(newData) as Sprite;
    this.editor.execute(new SetMaterialCommand(this.editor,object,text.material,0));
  }

  /**
   * 设置值
   * @param target uuid或Object3D对象
   * @param key 字段名
   * @param value 值
   */
  setValue<T extends Object3D,K extends keyof T>(target:string|T,key:K,value:any){
    let object:Object3D|null=null;
    if(target instanceof String){
      object=this.editor.scene.getObjectByProperty('uuid',target);
    }else if(target instanceof Object3D){
      object=target;
    }
    if(object!==null){
      this.editor.execute(new SetValueCommand(this.editor,object,key as any,value));
    }
  }

  /**
   * 选中物体对象事件
   * @param fn 
   */
  onSelected(fn:(object:Object3D)=>void){
    this.editor.signals.objectSelected.add(fn);
  }

  get background():string{
    return this.editor.project.background;
  }
  set background(color:string){
    this.editor.project.background=color;
  }
  get control():boolean{
    return this.editor.project.control;
  }
  set control(control:boolean|Control){
    this.editor.project.control=control;
  }

  get data():string{
    return JSON.stringify(this.editor.toJSON(),(key, value) => {
      if(key.startsWith('_')){
        return;
      }
      try{ 
        if (value instanceof Number) {
          return parseFloat(value.toFixed(2));
        }
      }catch(e){
        console.error(e);
      }
      return value;
    },0);
    /* return compress(JSON.stringify(this.editor.toJSON(),(key, value) => {
      if(key.startsWith('_')){
        return;
      }
      try{ 
        if (value instanceof Number) {
          return parseFloat(value.toFixed(2));
        }
      }catch(e){
        console.error(e);
      }
      return value;
    },0)); */
  }

  open(data?:Uint8Array){
    if(data){
      decompress(data).then(d=>{
        const json=new TextDecoder().decode(d);
        this.openJson(JSON.parse(json));
      }).catch(error=>{
        console.error('打开失败',error);
        throw error;
      });
      /* const json=new TextDecoder().decode(decompress(data));
      try {
        this.openJson(JSON.parse(json));
      } catch (error) {
        throw error; 
      } */
    }else{
      throw new Error('不能打开空数据');
    }
  }

  openJson(json:any){
    this.editor.fromJSON(json);
  }

  importFiles(){
    this.editor.importFiles();
  }

  /**
   * 选择文件系统的tezero文件打开场景
   */
  load(){
    const fileInput = document.createElement( 'input' );
		fileInput.multiple = false;
		fileInput.type = 'file';
		fileInput.accept='.tezero';
		fileInput.addEventListener( 'change', ()=> {
      const fr=new FileReader();
      fr.onload=(ev)=>{
        this.open(new Uint8Array(ev.target.result as ArrayBuffer));
      }
      fr.readAsArrayBuffer(fileInput.files[0]);

		} );
		fileInput.click();
  }

  /**
   * 下载场景数据文件
   * @param filename 下载保存的文件名
   */
  download(filename:string='tezero'){
    const encoder = new TextEncoder();
    const uint8Array = encoder.encode(this.data);
    compress(uint8Array).then(result=>{
      const blob = new Blob([result], { type: 'text/plain' });
      const downloadUrl = URL.createObjectURL(blob);
      const downloadLink = document.createElement('a');
      downloadLink.href = downloadUrl;
      downloadLink.download = filename+'.tezero';
      downloadLink.click();
      URL.revokeObjectURL(downloadUrl);
    });
    
  }

  /**
   * 获取场景压缩数据
   */
  async toData(){
    const encoder = new TextEncoder();
    const uint8Array = encoder.encode(this.data);
    const result=await compress(uint8Array);
    return result;
  }



  /**
   * 场景视图
   */
  static View=View;

  /**
   * 拖动事件dataTransfer
   */
  static readonly DragData='TezeroDragData';
  static draggable(dom:HTMLElement,data:any){
    dom.setAttribute('draggable','true');
    dom.ondragstart=(e:DragEvent)=>{
        e.dataTransfer?.setData(Tezero.DragData,JSON.stringify(data));
    }
  }
  /**
   * 创建材质
   * @param materialType 材质类型
   * @param material 材质参数
   * @returns 
   */
  static createMaterial(materialType?:MaterialType,material?:MaterialParameters):Material|undefined{
    materialType=materialType||MaterialType.Standard;
    if(materialType===MaterialType.Basic){
        return new MeshBasicMaterial(material);
    }
    if(materialType===MaterialType.Standard){
        return new MeshStandardMaterial(material);
    }
    if(materialType===MaterialType.Lambert){
        return new MeshLambertMaterial(material);
    }
  }
  static MaterialType=MaterialType;
  static THREE=THREE;
}

(window as any).Tezero=Tezero;

function setBaseAttr(object:THREE.Object3D,data:TezeroCommponent){
  if(!data.userData){
    data.userData={};
  }
  object.userData.type = data.type;
  if(data.position){
    object.position.copy(data.position);
  }
  if(data.scale){
    object.scale.copy(data.scale);
  }
  if(data.name){
    object.name=data.name;
  }
  if(data.animations){
    object.userData.animations=data.animations;
  }
}

export interface TezeroCommponents{
  [key:string]:(componentData:TezeroCommponent)=>THREE.Object3D|Promise<THREE.Object3D>
}
export interface TezeroCommponent{
  type:string;
  material?:THREE.MaterialParameters;
  materialType?:MaterialType;
  
  [key:string]:any;
}

export const defaultComponents={...compoennts};


export interface TezeroAnimation{
  key:string,
  to:any,
  repeat?:number,
  start?:boolean,
  delay?:number,
  yoyo?:boolean,
  easing?:string
}

interface Project{
  background:string,
  control:boolean,

}


export interface Control{
  /**
     * How far you can dolly in ( PerspectiveCamera only ).
     * @default 0
     */
   minDistance?: number;

   /**
    * How far you can dolly out ( PerspectiveCamera only ).
    * @default Infinity
    */
   maxDistance?: number;

   /**
    * How far you can zoom in ( OrthographicCamera only ).
    * @default 0
    */
   minZoom?: number;

   /**
    * How far you can zoom out ( OrthographicCamera only ).
    * @default Infinity
    */
   maxZoom?: number;

   /**
    * How far you can orbit vertically, lower limit.
    * Range is 0 to Math.PI radians.
    * @default 0
    */
   minPolarAngle?: number;

   /**
    * How far you can orbit vertically, upper limit.
    * Range is 0 to Math.PI radians.
    * @default Math.PI.
    */
   maxPolarAngle?: number;

   /**
    * How far you can orbit horizontally, lower limit.
    * If set, the interval [ min, max ]
    * must be a sub-interval of [ - 2 PI, 2 PI ],
    * with ( max - min < 2 PI ).
    * @default Infinity
    */
   minAzimuthAngle?: number;

   /**
    * How far you can orbit horizontally, upper limit.
    * If set, the interval [ min, max ] must be a sub-interval
    * of [ - 2 PI, 2 PI ], with ( max - min < 2 PI ).
    * @default Infinity
    */
   maxAzimuthAngle?: number;

   /**
    * Set to true to enable damping (inertia), which can
    * be used to give a sense of weight to the controls.
    * Note that if this is enabled, you must call
    * .update () in your animation loop.
    * @default false
    */
   enableDamping?: boolean;

   /**
    * The damping inertia used if .enableDamping is set to true.
    * Note that for this to work,
    * you must call .update () in your animation loop.
    * @default 0.05
    */
   dampingFactor?: number;

   /**
    * Enable or disable zooming (dollying) of the camera.
    * @default true
    */
   enableZoom?: boolean;

   /**
    * Speed of zooming / dollying.
    * @default 1
    */
   zoomSpeed?: number;

   /**
    * Setting this property to `true` allows to zoom to the cursor's position.
    * @default false
    */
   zoomToCursor?: boolean;

   /**
    * Enable or disable horizontal and
    * vertical rotation of the camera.
    * Note that it is possible to disable a single axis
    * by setting the min and max of the polar angle or
    * azimuth angle to the same value, which will cause
    * the vertical or horizontal rotation to be fixed at that value.
    * @default true
    */
   enableRotate?: boolean;

   /**
    * Speed of rotation.
    * @default 1
    */
   rotateSpeed?: number;

   /**
    * Enable or disable camera panning.
    * @default true
    */
   enablePan?: boolean;

   /**
    * Speed of panning.
    * @default 1
    */
   panSpeed?: number;

   /**
    * Defines how the camera's position is translated when panning.
    * If true, the camera pans in screen space. Otherwise,
    * the camera pans in the plane orthogonal to the camera's
    * up direction. Default is true for OrbitControls; false for MapControls.
    * @default true
    */
   screenSpacePanning?: boolean;

   /**
    * How fast to pan the camera when the keyboard is used.
    * Default is 7.0 pixels per keypress.
    * @default 7
    */
   keyPanSpeed?: number;

   /**
    * Set to true to automatically rotate around the target.
    * Note that if this is enabled, you must call
    * .update () in your animation loop.
    */
   autoRotate?: boolean;

   /**
    * How fast to rotate around the target if .autoRotate is true.
    * Default is 2.0, which equates to 30 seconds per orbit at 60fps.
    * Note that if .autoRotate is enabled, you must call
    * .update () in your animation loop.
    * @default 2
    */
   autoRotateSpeed?: number;
}


function runSyncFetch(func:Function){
  const cache = [];
  let i = 0;
  const _originalFetch = window.fetch;
  window.fetch = (input: RequestInfo | URL, init?: RequestInit)=>{
    
    if(cache[i]){
      if(cache[i].status === 'fulfilled'){
        return cache[i].data
      }
      else if(cache[i].status === 'rejected'){
        throw cache[i].err
      }
    }
    
    const result = {
      status:'padding',
      data:null,
      err:null
    };
    cache[i++] = result;
    
    const prom =  _originalFetch(input,init).then( res=>res.arrayBuffer()).then(
    res=>{
      result.status = 'fulfilled';
      result.data = res;
    },err=>{
      result.status = 'rejected';
      result.data = err;
    }
    )
    
    throw prom
  };
  try{
    func()
  }catch(err){
    
    if(err instanceof Promise){
      const reRun = ()=>{
        i=0;
        func()
      }
      err.then(reRun,reRun)
    }
  }finally{
    // window.fetch=_originalFetch;
  }
   
}